package com.ubs.lingua.buildtool.cs;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.rmi.RemoteException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import javax.annotation.PostConstruct;

import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
import org.springframework.jmx.export.annotation.ManagedResource;

import com.healthmarketscience.rmiio.RemoteInputStream;
import com.healthmarketscience.rmiio.RemoteInputStreamClient;
import com.healthmarketscience.rmiio.RemoteInputStreamMonitor;
import com.healthmarketscience.rmiio.RemoteInputStreamServer;
import com.healthmarketscience.rmiio.SimpleRemoteInputStream;
import com.ubs.lingua.buildtool.cs.parameters.BuildParameters;
import com.ubs.lingua.buildtool.cs.utils.CommonUtils;
import com.ubs.lingua.buildtool.cs.utils.RepositoryUtils;

/**
 * Singleton Spring bean implementing the public facade interface for the Lingua C# Build Agent.
 */
@ManagedResource(objectName = "Lingua:type=BuildTool,name=csBuildAgent", log = true, logFile = "lingua-cs-builder.log", currencyTimeLimit = -1, persistPolicy = "Never", description = "Lingua Build Tool agent for .NET assemblies")
public class CsBuilderFacadeImpl implements CsBuilderFacade {

    private static final Log LOG = LogFactory.getLog(CsBuilderFacadeImpl.class);

    /**
     * The working repository path.
     */
    private String repositoryPath;

    /**
     * If the Lingua C# Build Agent is in debug mode
     */
    private boolean debugMode;

    @Autowired(required = true)
    private CsBuilder csBuilder;

    /**
     * Date-time pattern which is used in order to construct the unique build batch ID.
     */
    private static final String DATE_FORMAT = "yyyy-MM-dd'T'HHmmss.SSSZ";

    /**
     * Concurrent map which will store all running and finished, but not yet retrieved, batches.
     */
    private final ConcurrentMap<String, Future<Resource>> batches = new ConcurrentHashMap<String, Future<Resource>>();

    /**
     * Performs the working repository cleanup at bean's instantiation time. 
     */
    @PostConstruct
    public void clenupRepository() throws IOException {
        final File repository = new FileSystemResource(repositoryPath).getFile();
        // creating repository directory if not exist
        if (!repository.exists() && !repository.mkdirs()) {
            throw new IOException("Unable to create repository directory at " + repositoryPath);
        }

        if (!debugMode) {
            // repository cleanup
            try {
                FileUtils.cleanDirectory(repository);
            }
            catch (IOException ex) {
                LOG.error("Unable to cleanup repository", ex);
            }
        }
    }

    @ManagedOperation(description = "Starts build operation")
    @ManagedOperationParameters({
            @ManagedOperationParameter(name = "waitTime", description = "Delay time"),
            @ManagedOperationParameter(name = "sourcePackage", description = "ZIP package containing C# sources and all dependencies") })
    public String buildAssembly(final BuildParameters parameters, final RemoteInputStream sourcePackage)
            throws IOException {
        LOG.info("New build requested");
        final String batchId = CommonUtils.getCurrentDate(DATE_FORMAT) + "-" + CommonUtils.getRandomNb(100000);
        final String batchLocation = RepositoryUtils.getBatchPath(repositoryPath, batchId, null);
        final String packagePath = RepositoryUtils.getBatchPath(repositoryPath, batchId,
                                                                RepositoryUtils.PACKAGE_FILE_NAME);

        LOG.info("Reading batch " + batchId + " from client");
        final InputStream fileStream = RemoteInputStreamClient.wrap(sourcePackage);
        try {
            // crating batch directory and checking whether it is unique
            RepositoryUtils.ensureUnique(batchLocation);
            final OutputStream outputStream = new FileOutputStream(new FileSystemResource(packagePath).getFile());
            try {
                final byte[] buffer = new byte[4096];
                for (int n; (n = fileStream.read(buffer)) != -1;) {
                    outputStream.write(buffer, 0, n);
                }
            }
            finally {
                outputStream.close();
            }
        }
        finally {
            fileStream.close();
        }

        LOG.info("Package for batch " + batchId + " uploaded, starting build process");
        try {
            batches.put(batchId, csBuilder.build(parameters, packagePath));
        }
        catch (InterruptedException e) {
            // this should never happen here
            LOG.warn("Accidentally interrupted while starting batch " + batchId, e);
        }
        LOG.info("Build called for batch " + batchId);

        return batchId;
    }

    @ManagedOperation(description = "Returns result of assembly building process")
    @ManagedOperationParameters({ @ManagedOperationParameter(name = "batchId", description = "Batch ID") })
    public RemoteInputStream getAssembly(final String batchId) throws InterruptedException, ExecutionException,
            RemoteException, IOException {
        if (null == batchId) {
            throw new IllegalArgumentException("Batch ID is null");
        }
        final Future<Resource> batchResult = batches.remove(batchId);
        if (null == batchResult) {
            throw new IllegalArgumentException("Batch " + batchId + " not found");
        }
        final String batchPath = RepositoryUtils.getBatchPath(repositoryPath, batchId, null);
        if (debugMode) {
            LOG.info("DEBUG mode is turned on, batch package will be retained in the " + batchPath + " directory");
        }
        boolean transferStarted = false;
        try {
            // exception will be thrown after this line execution if asynchronous task finished its work with exception
            final InputStream taskResult = batchResult.get().getInputStream();
            LOG.info("Retrieving batch " + batchId + " result");

            // creating monitor which will perform repository cleanup when package is sent back to client
            transferStarted = true;
            final RemoteInputStreamMonitor monitor = new RemoteInputStreamMonitor() {
                public void closed(final RemoteInputStreamServer stream, final boolean clean) {
                    if (!debugMode && !RepositoryUtils.deleteQuietly(batchPath)) {
                        LOG.error("RemoteInputStreamMonitor: Unable to remove batch's directory from the repository "
                                + batchPath);
                    }
                }
            };

            // sending assembly package to client
            return new SimpleRemoteInputStream(new BufferedInputStream(taskResult), monitor).export();
        }
        finally {
            if (!debugMode && !transferStarted && !RepositoryUtils.deleteQuietly(batchPath)) {
                LOG.error("Unable to remove batch's directory from the repository " + batchPath);
            }
        }
    }

    @ManagedOperation(currencyTimeLimit = 10, description = "Returns true if assembly building process completed. Completion may be due to normal termination, an exception, or cancellation -- in all of these cases, this method will return true.")
    @ManagedOperationParameters({ @ManagedOperationParameter(name = "batchId", description = "Batch ID") })
    public boolean isBatchDone(final String batchId) {
        if (null == batchId) {
            throw new IllegalArgumentException("Batch ID is null");
        }
        final Future<Resource> batchResult = batches.get(batchId);
        if (null == batchResult) {
            throw new IllegalArgumentException("Batch " + batchId + " not found");
        }
        return batchResult.isDone();
    }

    @ManagedAttribute(currencyTimeLimit = 10, description = "The source repository path")
    public String getRepositoryPath() {
        return repositoryPath;
    }

    public void setRepositoryPath(final String repositoryPath) {
        this.repositoryPath = repositoryPath;
    }

    @ManagedAttribute(currencyTimeLimit = 10, description = "Debug mode switched on/off")
    public boolean isDebugMode() {
        return debugMode;
    }

    @ManagedAttribute(description = "Enables/Disables debug mode")
    public void setDebugMode(final boolean debugMode) {
        LOG.info("DEBUG mode " + (debugMode ? "enabled" : "disabled"));
        this.debugMode = debugMode;
    }

}
